﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using Kokomo.PeInspector.Reflection.Disassembly;
using System.Threading;
using Kokomo.PeInspector.CliMetadata;
using Kokomo.Mirror;
using Kokomo.Mirror.CliMetadata;
using Kokomo.Mirror.PeFormat;
using System.IO;
using System.Xml.XPath;
using System.Xml;
using System.Windows;
using Kokomo.Mirror.CliMetadata.Tables;
using System.Threading.Tasks;
using System.Diagnostics;
using System.Windows.Media;

namespace Kokomo.PeInspector.Reflection
{
	public class AssemblyIndex
	{
		static AssemblyIndex()
		{
		}

		public static AssemblyIndex Create(
			Assembly assembly,
			IAssemblyLoadContext loadContext
			)
		{
			return new AssemblyIndex(assembly);
		}

		public Assembly Assembly { get; private set; }

		protected AssemblyIndex(Assembly assembly)
		{
			if (assembly == null) throw new ArgumentNullException("assembly");
			this.Assembly = assembly;

			// Load types
			Type[] types = null;
			try
			{
				types = this.Assembly.GetTypes();
			}
			catch (System.Reflection.ReflectionTypeLoadException e)
			{
				types = Array.FindAll(e.Types, EnumerableExtensions.IsNonNull);
			}
			this.Types = types;

			this.NamespaceGroups = this.CreateNamespaceGroups();
			this._namespaceTable = this.NamespaceGroups.ToDictionary(n => n.Namespace ?? string.Empty);
			//this.Members = this.CreateMemberList();

			string path = assembly.Location;
			IHaveMetadata metadataHolder = assembly as IHaveMetadata;
			if (metadataHolder != null)
			{
				this.Metadata = metadataHolder.Metadata;
			}
			else
			{
				PeModule pe = PeModule.Open(path);
				this.Metadata = pe.MetadataContainer;
			}
		}

		public MetadataContainer Metadata { get; private set; }
		//public PeModule MainPeModule { get; private set; }
		public Type[] Types { get; private set; }
		public IEnumerable<NamespaceScope> NamespaceGroups { get; private set; }

		private IDictionary<string, NamespaceScope> _namespaceTable;
		public NamespaceScope GetNamespaceScope(string @namespace)
		{
			NamespaceScope namespaceScope;
			this._namespaceTable.TryGetValue(@namespace, out namespaceScope);
			return namespaceScope;
		}


		//public IEnumerable<PropertyInfo> FindProperties(Func<PropertyRow, bool> predicate)
		//{
		//    if (predicate == null) throw new ArgumentNullException("predicate");

		//    return this.MainPeModule.MetadataContainer.PropertyTable.Where(predicate).Select(this.Assembly;
		//}

		private IEnumerable<TMember> GetAllMembersOfType<TMember>(
			MetadataTableNumber tableNumber,
			Func<int, TMember> tokenResolver
			)
			where TMember : class
		{
			return this.FindMembers<IMetadataRow, TMember>(tableNumber, tokenResolver, null);
		}
		private IEnumerable<TMember> FindMembers<TMemberRow, TMember>(
			MetadataTableNumber tableNumber,
			Func<int, TMember> tokenResolver,
			Func<TMemberRow, bool> predicate
			)
			where TMember : class
			where TMemberRow : IMetadataRow
		{
			Module module = this.Assembly.ManifestModule;
			int tablePrefix = (int)tableNumber << 24;
			MetadataTable table = this.Metadata.GetTable(tableNumber);
			int rowCount = table.RowCount;

			for (int rowNumber = 1; rowNumber <= rowCount; rowNumber++)
			{
				int token = rowNumber | tablePrefix;
				bool include = true;
				if (predicate != null)
				{
					TMemberRow row = (TMemberRow)table.GetItem(rowNumber - 1);
					include = predicate(row);
				}

				if (include)
				{
					TMember method = null;
					try
					{
						method = tokenResolver(token);
					}
					catch
					{
						// Module.ResolveMethod sometimes throws NullReferenceException with a valid token
					}
					if (method != null) yield return method;
				}
			}
		}
		//private MethodInfo SafeResolveMethod(Module module, int token)
		//{
		//    // HACK: ResolveMethod fails with methods defined outside of type
		//    RuntimeMethodHandle methodHandle = module.ModuleHandle.GetRuntimeMethodHandleFromMetadataToken(token);
		//    RuntimeTypeHandle declaringTypeHandle=methodHandle.
		//}
		public IEnumerable<MethodBase> GetAllMethodBases()
		{
			Module module = this.Assembly.ManifestModule;
			return this.GetAllMembersOfType<MethodBase>(MetadataTableNumber.MethodDef, module.ResolveMethod);
		}
		public IEnumerable<FieldInfo> GetConstantFields()
		{
			Module module = this.Assembly.ManifestModule;
			return this.FindMembers<FieldRow, FieldInfo>(
				MetadataTableNumber.Field,
				module.ResolveField,
				IsLiteralField
				);
		}
		private static bool IsLiteralField(FieldRow fieldRow)
		{
			bool isLiteral = fieldRow.Flags.HasFlag(FieldAttributes.Literal);
			return isLiteral;
		}
		public int FieldCount
		{
			get { return this.Metadata.GetTable(MetadataTableNumber.Field).RowCount; }
		}
		public int MethodBaseCount
		{
			get { return this.Metadata.GetTable(MetadataTableNumber.MethodDef).RowCount; }
		}

		public IEnumerable<FieldInfo> GetAllFields()
		{
			Module module = this.Assembly.ManifestModule;
			return this.GetAllMembersOfType<FieldInfo>(MetadataTableNumber.Field, module.ResolveField);
		}
		public IEnumerable<PropertyInfo> GetAllProperties()
		{
			Module module = this.Assembly.ManifestModule;
			return this.GetAllMembersOfType<PropertyInfo>(MetadataTableNumber.Property, t => (PropertyInfo)module.ResolveMember(t));
		}
		public IEnumerable<EventInfo> GetAllEvents()
		{
			Module module = this.Assembly.ManifestModule;
			return this.GetAllMembersOfType<EventInfo>(MetadataTableNumber.Event, t => (EventInfo)module.ResolveMember(t));
		}
		public IEnumerable<Type> GetAllTypes()
		{
			Module module = this.Assembly.ManifestModule;
			return this.GetAllMembersOfType<Type>(MetadataTableNumber.TypeDef, module.ResolveType);
		}

		private IEnumerable<NamespaceScope> CreateNamespaceGroups()
		{
			var q =
				from t in this.Types
				where (t.DeclaringType == null)
				orderby t.Name
				group t by t.Namespace into n
				orderby n.Key
				select new NamespaceScope(this.Assembly, n.Key, n);

			return q.ToArray();
		}
		public IEnumerable<MemberInfo> GetMemberList()
		{
			BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly;
			var members = this.Types.SelectMany(t => t.GetMembers(bindingFlags));

			return members;
		}
		//private MemberInfo[] CreateMemberList()
		//{
		//    BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic;
		//    MemberInfo[] members = this.Types.SelectMany(t => t.GetMembers(bindingFlags)).ToArray();

		//    return members;
		//}

		//private XPathNavigator DocumentationNavigator { get; private set; }
		private XPathNavigator GetDocumentationNavigator()
		{
			XPathNavigator documentationNavigator = null;

			string xmlDocumentationFileName = Path.ChangeExtension(this.Assembly.Location, ".xml");
			if (!File.Exists(xmlDocumentationFileName))
			{
				xmlDocumentationFileName = Path.ChangeExtension(Path.Combine(System.Environment.GetFolderPath(System.Environment.SpecialFolder.ProgramFiles), @"Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0", Path.GetFileName(this.Assembly.Location)), ".xml");
			}
			if (File.Exists(xmlDocumentationFileName))
			{
				try
				{
					XPathDocument document = new XPathDocument(xmlDocumentationFileName);
					documentationNavigator = document.CreateNavigator();
				}
				catch { /* Invalid documentation file */ }
			}

			return documentationNavigator;
		}

		public XmlDocument GetDocumentationFor(MemberInfo member)
		{
			if (member == null) throw new ArgumentNullException("member");

			XPathNavigator documentationNavigator = this.GetDocumentationNavigator();
			if (documentationNavigator == null) return null;

			string key = this.GetMemberXmlDocKey(member);
			if (key == null) return null;

			string nodePath = string.Format("/doc/members/member[@name='{0}']", key);
			var memberNode = this.GetDocumentationNavigator().SelectSingleNode(nodePath);
			if (memberNode == null) return null;

			string xml = memberNode.OuterXml;
			XmlDocument document = new XmlDocument();
			document.LoadXml(xml);

			return document;
		}
		protected string GetMemberXmlDocKey(MemberInfo member, bool includePrefix = true)
		{
			if (member == null) throw new ArgumentNullException("member");

			StringBuilder nameBuilder = new StringBuilder();

			{
				FieldInfo field = member as FieldInfo;
				if (
					(field != null)
					&& field.IsStatic
					&& (field.FieldType == typeof(DependencyProperty))
					&& (field.Name.EndsWith("Property"))
					)
				{
					if (includePrefix)
						nameBuilder.Append("P:");

					string typeKey = this.GetMemberXmlDocKey(field.DeclaringType, false);
					nameBuilder.Append(typeKey);
					nameBuilder.Append(".");
					nameBuilder.Append(field.Name.Substring(0, field.Name.Length - "Property".Length));

					return nameBuilder.ToString();
				}
			}

			if (includePrefix)
			{
				string prefix;
				switch (member.MemberType)
				{
					case MemberTypes.Constructor:
					case MemberTypes.Method:
						prefix = "M";
						break;
					case MemberTypes.Event:
						prefix = "E";
						break;
					case MemberTypes.Field:
						prefix = "F";
						break;
					case MemberTypes.NestedType:
					case MemberTypes.TypeInfo:
						prefix = "T";
						break;
					case MemberTypes.Property:
						prefix = "P";
						break;
					default:
						return null;
				}
				nameBuilder.Append(prefix);
				nameBuilder.Append(":");
			}

			Type type = member as Type;
			if (type != null)
			{
				if (type.IsNested)
				{
					nameBuilder.Append(this.GetMemberXmlDocKey(type.DeclaringType, false));
					nameBuilder.Append("+");
					nameBuilder.Append(type.Name);
				}
				else
				{
					nameBuilder.Append(type.FullName);
				}
			}
			else
			{
				nameBuilder.Append(this.GetMemberXmlDocKey(member.DeclaringType, false));
				nameBuilder.Append(".");
				nameBuilder.Append(member.Name.Replace('.', '#'));

				MethodBase method = member as MethodBase;
				if (method != null)
				{
					ParameterInfo[] parameters = method.GetParameters();
					if (parameters.Length > 0)
					{
						nameBuilder.Append("(");
						for (int i = 0; i < parameters.Length; i++)
						{
							if (i > 0) nameBuilder.Append(",");

							ParameterInfo parameter = parameters[i];
							nameBuilder.Append(this.GetMemberXmlDocKey(parameter.ParameterType, false));
						}
						nameBuilder.Append(")");
					}
				}
			}

			string key = nameBuilder.ToString();
			return key;
		}

		internal IList<StatisticItem> GetStatistics()
		{
			Type[] types = this.GetAllTypes().ToArray();

			StatisticItem[] statisticItems = new StatisticItem[] {
				BuildStatistic("Types", MetadataIconKeys.Class, types, t => t.IsPublic),
				BuildStatistic("Classes", MetadataIconKeys.Class, types.Where(t => t.Kind() == TypeKind.Class), t => t.IsPublic),
				BuildStatistic("Structures", MetadataIconKeys.Struct, types.Where(t => t.Kind() == TypeKind.Structure), t => t.IsPublic),
				BuildStatistic("Interfaces", MetadataIconKeys.Interface, types.Where(t => t.Kind() == TypeKind.Interface), t => t.IsPublic),
				BuildStatistic("Delegates", MetadataIconKeys.Delegate, types.Where(t => t.Kind() == TypeKind.Delegate), t => t.IsPublic),
				BuildStatistic("Enumerations", MetadataIconKeys.Enum, types.Where(t => t.Kind() == TypeKind.Enumeration), t => t.IsPublic),
				BuildStatistic("Methods", MetadataIconKeys.Method, this.GetAllMethodBases(), m => m.IsPublic),
				BuildStatistic("Fields", MetadataIconKeys.Field, this.GetAllFields(), f => f.IsPublic),
				BuildStatistic("Properties", MetadataIconKeys.Property, this.GetAllProperties(), p => p.GetAccessors(true)[0].IsPublic),
				BuildStatistic("Events", MetadataIconKeys.Event, this.GetAllEvents(), p => p.GetAddMethod(true).IsPublic),
			};

			return statisticItems;
		}

		private static StatisticItem BuildStatistic<TItem>(
			string label,
			string iconKey,
			IEnumerable<TItem> items,
			Func<TItem, bool> isPublic
			)
		{
			int publicCount = 0;
			int nonPublicCount = 0;
			int totalCount = 0;

			foreach (var item in items)
			{
				if (isPublic(item))
					publicCount++;
				else
					nonPublicCount++;

				totalCount++;
			}

			ImageSource icon = null;
			if (iconKey != null)
				icon = AssemblyAnalyzerEditor.MetadataIconProvider.GetImage(iconKey);

			StatisticItem stat = new StatisticItem(
				label,
				icon,
				publicCount,
				nonPublicCount,
				totalCount
				);
			return stat;
		}
	}
}
